package org.tote.service.user;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.NullArgumentException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.TextEscapeUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.tote.command.IPInfoJsonCommand;
import org.tote.command.UserCommandLight;
import org.tote.dao.DaoFacade;
import org.tote.exception.ElementNotFoundException;
import org.tote.exception.UserAlreadyExistException;
import org.tote.model.Role;
import org.tote.model.RoleType;
import org.tote.model.User;
import org.tote.model.UserLight;
import org.tote.service.MailBusinessService;
import org.tote.util.Constant;
import org.tote.util.TokenGenerator;

@Service
@Transactional
public class UserServiceImpl implements UserService {

	@Resource(name = "sessionRegistry")
	private SessionRegistryImpl sessionRegistry;

	// shouldn't be longer than length of UUID
	private static final int GENERATED_PASSWORD_LENGHT = 13;

	// the login of the user that will sign system notifications if none specified
	public static final String MAINTENANCE_USER = "MaintenanceUser";
	public static final long MAINTENANCE_USER_ID = -1;
	public static final String GUEST_USER = "Guest";

	@Autowired
	private DaoFacade daoFacade;

	@Autowired
	private ProviderManager authenticationManager;

	@Autowired
	private MailBusinessService mailService;

	private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

	@Override
	public void saveOrUpdateUser(User user) {
		if (user == null) {
			throw new NullArgumentException("User for save or update can't be NULL!");
		}
		if (user.getId() != null) {//userExits(user.getUsername())) {
			daoFacade.update(user);

		} else {
			daoFacade.save(user);
		}
	}

	@Override
	public void registerUser(User user) throws UserAlreadyExistException {
		if (user == null) {
			throw new NullArgumentException("User for registration can't be NULL!");
		}

		if (userExits(user.getUsername())) {
			throw new UserAlreadyExistException(user);
		}

		if (!Constant.matchPassword(user.getPassword()))
			throw new RuntimeException(Constant.PASSWORD_ERROR);

		user.setStatus(Constant.STATUS_ENTERED);
		user.setToken(TokenGenerator.generateRandomToken());
		user.setUpdTime(new Date());
		user.setUpdUser(getMaintenanceUser());
		// Backup user password for mail sending
		User userForMail = new User(user);
		hashPassword(user);
		daoFacade.save(user);
		daoFacade.save(new Role(RoleType.EVERYONE, user));

		// Sending email to user
		mailService.sendPostRegistrationMail(userForMail, getMaintenanceUserLight());
	}

	@Override
	public void adminCreateUser(User user) throws UserAlreadyExistException {
		if (user == null) {
			throw new NullArgumentException("User for registration can't be NULL!");
		}

		if (userExits(user.getUsername())) {
			throw new UserAlreadyExistException(user);
		}
		user.setStatus(Constant.STATUS_READY);
		hashPassword(user);
		daoFacade.save(user);
	}

	@Override
	public void restorePassword(String login) {
		if (login == null) {
			throw new NullArgumentException("User to restore password can't be NULL!");
		}
		User user = daoFacade.getUserDao().findByLogin(login);
		if (user == null) throw new RuntimeException("User doesn't exists");
		if (!user.getEnabled()) throw new RuntimeException("User acoount is locked");

		String newPassword = UUID.randomUUID().toString().substring(0, GENERATED_PASSWORD_LENGHT);
		user.setPassword(newPassword);
		hashPassword(user);
		saveOrUpdateUser(user);

		mailService.sendRestorePasswordMail(user, newPassword, getMaintenanceUserLight());
	}

	@Override
	public void hashPassword(User user) {
		// Encoding password before saving
		if (user.getPassword() != null) {
			user.setPassword(TokenGenerator.convertToSHA256(user.getPassword()));
		}
	}

	@Override
	public UserLight getMaintenanceUserLight() {
		UserLight userLight = new UserLight();
		userLight.setId(MAINTENANCE_USER_ID);
		userLight.setName(MAINTENANCE_USER);
		return userLight;
	}

	private User getMaintenanceUser() {
		User user = new User();
		user.setId(MAINTENANCE_USER_ID);
		user.setName(MAINTENANCE_USER);
		return user;
	}

	// CurrentAuthorizedUser
	@Override
	public User getCurrentAuthorizedUser() {
		CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder
				.getContext().getAuthentication().getPrincipal();
		if (customUserDetails == null) return null;
		User user = daoFacade.getUserById(customUserDetails.getId());
		return user;
	}

	@Override
	public UserLight getCurrentAuthorizedUserLight() {
		CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder
				.getContext().getAuthentication().getPrincipal();
		UserLight userLight = new UserLight();
		userLight.setId(customUserDetails.getId());
		userLight.setName(customUserDetails.getName());
		return userLight;
	}

	@Override
	public Long getCurrentAuthorizedUserId() {
		return ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication()
				.getPrincipal()).getId();
	}

	// CurrentAuthorizedUser end
	// Any User
	@Override
	public User getUserById(Long id) {
		if (id == null) throw new NullArgumentException("User's id can't be NULL!");
		User user = this.daoFacade.getUserById(id);
		if (user == null) throw new ElementNotFoundException(User.class, String.valueOf(id));
		return user;
	}

	@Override
	public User getUser(UserLight user) {
		return getUserById(user.getId());
	}

	@Override
	public UserLight getUserLightById(Long id) {
		if (id == null) throw new NullArgumentException("User's id can't be NULL!");
		if (id == getCurrentAuthorizedUserId()) return getCurrentAuthorizedUserLight();
		UserLight user = this.daoFacade.getUserLightById(id);
		if (user == null) throw new ElementNotFoundException(UserLight.class, String.valueOf(id));
		return user;
	}

	@Override
	public User getUserByOpenId(String id) {
		return daoFacade.getUserDao().findByOpenId(id);
	}

	// Any User end

	@Override
	public boolean userExits(String login) {
		if (login == null) {
			throw new NullArgumentException("'login's argument of user can't be NULL!");
		}

		return this.daoFacade.getUserDao().findByLogin(login) != null;
	}

	@Override
	public void removeUser(Long id) {
		if (id == null) {
			throw new NullArgumentException("User's id can't be NULL!");
		}
		User user = this.daoFacade.getUserById(id);

		if (user == null) {
			throw new ElementNotFoundException(User.class, String.valueOf(id));
		}

		this.daoFacade.remove(user);
	}

	@Override
	public List<User> getAllUsers() {
		return this.daoFacade.getUserDao().getAllUsers();
	}

	@Override
	public List<User> getFilteredUsersPage(int page, int limit, String usersFilter) {
		return this.daoFacade.getUserDao().getFilteredUsersPage(page, limit, usersFilter);
	}

	@Override
	public void removeUserList(List<String> nameList) {
		if (nameList == null) {
			throw new NullArgumentException("List of ID's for removing users can't be NULL!");
		}
		daoFacade.getUserDao().removeList(nameList);
	}

	@Override
	public List<User> getUserListExcluding(List<String> nameList) {
		if (nameList == null) {
			throw new NullArgumentException("List of ID's for returning users can't be NULL!");
		}
		return (nameList.isEmpty() ? daoFacade.getUserDao().getAllUsers() : daoFacade.getUserDao()
				.getUserListExcluding(nameList));
	}

	@Override
	public List<User> getFilteredUsersExcludingPage(List<Long> excludeList, int page, int limit,
			String usersFilter) {
		if (excludeList == null) {
			throw new NullArgumentException("List of ID's for returning users can't be NULL!");
		}
		return daoFacade.getUserDao().getFilteredUsersExcludingPage(excludeList, page, limit,
				usersFilter);
	}

	@Override
	public List<User> getUserList(List<String> nameList) {
		if (nameList == null) {
			throw new NullArgumentException("List of users ID's can't be NULL!");
		}
		return daoFacade.getUserDao().getUserList(nameList);
	}

	@Override
	public boolean authorizeUserByToken(String token, HttpServletRequest request) {
		try {
			User user = daoFacade.getUserDao().findByToken(token);
			if (user == null)
				throw new RuntimeException("Invalid user token has been detected: " + token);

			// TODO Please note, that activation link is valid during the next 24 hours!

			user.setStatus(Constant.STATUS_USER_WAIT_CONFIRMATION);
			user.setToken(null);
			daoFacade.update(user);

			UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
					user.getUsername(), user.getPassword());

			user.setStatus(Constant.STATUS_READY); //must be enabled
			Authentication auth = authenticationManager.authenticate(authToken);
			user.setStatus(Constant.STATUS_USER_WAIT_CONFIRMATION);
			auth.setAuthenticated(false);
			SecurityContextHolder.getContext().setAuthentication(auth);
			// Place the last username attempted into HttpSession for views
			HttpSession session = request.getSession(false);

			if (session != null) {
				request.getSession().setAttribute(
						UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY,//SPRING_SECURITY_LAST_USERNAME_KEY,
						TextEscapeUtils.escapeEntities(user.getUsername()));
			}
			return true;
		} catch (Throwable e) {//BadCredentialsException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return false;
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public boolean disableUserById(Long id) {
		User user = getUserById(id);
		if (!user.getEnabled()) {
			LOGGER.info("User " + user.getName() + " is alredy disabled");
			return false;
		} else {
			user.setStatus(Constant.STATUS_ENTERED);
			saveOrUpdateUser(user);
			LOGGER.info("User '" + user.getName() + "' is sucessfully disabled by "
					+ getCurrentAuthorizedUserLight().getName());
			return true;
		}
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public boolean enableUserById(Long id) {
		User user = getUserById(id);
		if (user.getEnabled()) {
			LOGGER.info("User " + user.getName() + " is alredy enabled");
			return false;
		} else {
			user.setStatus(Constant.STATUS_READY);
			user.setToken(null);
			saveOrUpdateUser(user);
			LOGGER.info("User '" + user.getName() + "' is sucessfully enabled by "
					+ getCurrentAuthorizedUserLight().getName());
			return true;
		}
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public void modifyUserModyfiable(Long id, Boolean enabled, String name, String password) {
		User user = getUserById(id);
		if (name != null) {
			user.setName(name);
		}
		if ((password != null) && (!password.equals(""))) {
			user.setPassword(password);
			hashPassword(user);
		}
		if (enabled != null) {
			if (enabled) {
				enableUserById(id);
			} else {
				disableUserById(id);
			}
		}
		saveOrUpdateUser(user);
	}

	@Override
	public int getFilteredUsersMaxPage(int limit, String usersFilter) {
		return this.daoFacade.getUserDao().getFilteredUsersCount(usersFilter) / limit;
	}

	@Override
	public int getFilteredUsersExcludingCount(List<Long> excludeList, int limit, String usersFilter) {
		return this.daoFacade.getUserDao().getFilteredUsersExcludingCount(excludeList, usersFilter)
				/ limit;
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public List<UserCommandLight> getLoggedUsers() {
		List<UserCommandLight> users = new ArrayList<UserCommandLight>();
		for (Object principal : sessionRegistry.getAllPrincipals()) {
			if (!sessionRegistry.getAllSessions(principal, true).get(0).isExpired()
					&& principal instanceof CustomUserDetails) {
				CustomUserDetails userDetails = (CustomUserDetails) principal;
				UserCommandLight user = new UserCommandLight(userDetails.getId(),
						userDetails.getName());
				user.setInfo((userDetails.getLoggingTime() == null ? "" : Constant.DF
						.format(userDetails.getLoggingTime()))
						+ ", "
						+ userDetails.getClientBrowser()
						+ ", "
						+ userDetails.getClientIP()
						+ getCountryCity(userDetails.getClientIP()));
				users.add(user);
			}
		}
		return users;
	}

	private String getCountryCity(String ip) {
		String ipInfo = getIPInfo(ip);
		if (ipInfo != null) {
			IPInfoJsonCommand command = new IPInfoJsonCommand();
			ObjectMapper mapper = new ObjectMapper();
			try {
				ipInfo = ipInfo.replaceAll("\"IP\":", "\"ip\":");
				ipInfo = ipInfo.replaceAll("\"COUNTRY\":", "\"country\":");
				command = mapper.readValue(ipInfo, IPInfoJsonCommand.class);
				if ("Unknown".equalsIgnoreCase(command.getCountryName())
						&& "Unknown".equalsIgnoreCase(command.getCityName())) return "";
				return " (" + command.getCountryName() + "/" + command.getCityName() + ")";
			} catch (Exception e) {
				LOGGER.error("Can't parse ipInfo: " + ipInfo, e);
			}
		}
		return "";
	}

	/**
	 * http://www.easyjquery.com/detect-get-clients-ip-address-country-using-javascript-php/
	 * 
	 * @param ip
	 * @return JSON
	 *         {"IP":"88.223.209.101","continentCode":"EU","continentName":"Europe","countryCode2":"UA","COUNTRY":"UA",
	 *         "countryCode3"
	 *         :"UKR","countryName":"Ukraine","regionName":"Kyyiv","cityName":"Kiev","cityLatitude":50.4333
	 *         ,"cityLongitude":30.5167
	 *         ,"countryLatitude":49,"countryLongitude":32,"localTimeZone":"Europe\/Kiev","localTime":"16:54:36 pm"}
	 */
	private String getIPInfo(String ip) {
		try {

			String generate_URL = "http://api.easyjquery.com/ips/?ip=" + ip + "&full=true";
			URL data = new URL(generate_URL);
			URLConnection yc = data.openConnection();
			BufferedReader in = new BufferedReader(new InputStreamReader(yc.getInputStream()));
			String inputLine;
			StringBuilder temp = new StringBuilder();
			while ((inputLine = in.readLine()) != null) {
				temp.append(inputLine);
			}
			return temp.toString();
		} catch (Exception e) {
			LOGGER.error("Can't recieve IP info for IP: '" + ip + "'", e);
		}
		return null;
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public void logoutUser(Long id) {
		CustomUserDetails currUser = (CustomUserDetails) SecurityContextHolder.getContext()
				.getAuthentication().getPrincipal();
		if (currUser.getId().equals(id)) return;
		for (Object principal : sessionRegistry.getAllPrincipals()) {
			CustomUserDetails user = (CustomUserDetails) principal;
			if (user.getId().equals(id)) {
				sessionRegistry.getAllSessions(user, true).get(0).expireNow();
				break;
			}
		}
	}

	@Override
	@PreAuthorize("hasAnyRole('ADMINISTRATOR')")
	public void logoutAllUsers() {
		SimpleGrantedAuthority adminAutority = new SimpleGrantedAuthority(
				RoleType.ADMINISTRATOR.toString());
		for (Object principal : sessionRegistry.getAllPrincipals()) {
			org.springframework.security.core.userdetails.UserDetails userDetails = (org.springframework.security.core.userdetails.UserDetails) principal;
			if (!userDetails.getAuthorities().contains(adminAutority)) {
				sessionRegistry.getAllSessions(userDetails, true).get(0).expireNow();
			}
		}
	}

	@Override
	public List<String> getCurrentUserAutorities() {
		List<String> autorities = new ArrayList<String>();
		for (GrantedAuthority grantedAuthority : SecurityContextHolder.getContext()
				.getAuthentication().getAuthorities()) {
			autorities.add(grantedAuthority.getAuthority());
		}
		return autorities;
	}

	@Override
	public boolean isUserAdmin() {
		List<String> autorities = getCurrentUserAutorities();
		return autorities.contains("ADMINISTRATOR");
	}

	@Override
	public String getCurrentUserAuthoritiesAsString() {
		StringBuilder authorities = new StringBuilder();
		boolean isFirst = true;
		for (GrantedAuthority grantedAuthority : SecurityContextHolder.getContext()
				.getAuthentication().getAuthorities()) {
			if (!isFirst) {
				authorities.append(", ");
			}
			authorities.append("'");
			authorities.append(grantedAuthority.getAuthority());
			authorities.append("'");
			isFirst = false;
		}
		return authorities.toString();
	}

}